﻿//-------------------------------------------------------------------------------------------------
// <copyright file="SuperStarSplash.cs" company="Anthony Adam">
// Copyright (c) Anthony Adam 2012. All rights reserved.
// </copyright>
//-------------------------------------------------------------------------------------------------

using System.Linq;

namespace SuperStarSplash
{
  using System;
  using System.Collections;
  using System.Drawing;
  using System.Drawing.Drawing2D;
  using System.Globalization;
  using System.Threading;
  using System.Windows.Forms;

  /// <summary>
  /// Splash screen form with auto adjusting loader progress bar.
  /// </summary>
  public sealed partial class SuperStarSplash : Form
  {
    /// <summary>
    /// Reference to splash form for internal static function access.
    /// </summary>
    private static SuperStarSplash msFrmSplash;
    
    /// <summary>
    /// Thread the splash form will be running on (separate from application thread of caller)
    /// </summary>
    private static Thread msOThread;

    // Fade In/Out settings
    private double mDblOpacityIncrement = 0.05;
    private const double MDblOpacityDecrement = 0.08;
    private const int TimerInterval = 50;

    // Status & progress bar
    private string mSStatus = string.Empty;
    private double mDblCompletionFraction;
    private Rectangle mRProgress;

    // Progress smoothing
    private double mDblLastCompletionFraction;
    private double mDblPbIncrementPerTimerInterval = 0.0015;

    // Self-calibration support variables
    private bool mBFirstLaunch;
    private DateTime mDtStart;
    private bool mBDtSet;
    private int mIIndex = 1;
    private int mIActualTicks;
    
    private ArrayList mAlPreviousCompletionFraction;
    private readonly ArrayList mAlActualTimes = new ArrayList();

    private const string RegvaluePbMillisecondIncrement = "Increment";
    private const string RegvaluePbPercents = "Percents";
    
    /// <summary>
    /// Get the instance of the splash screen
    /// </summary>
    public static SuperStarSplash SplashForm => msFrmSplash;

    public static double Progress
    {
      get
      {
        return msFrmSplash?.mDblCompletionFraction ?? 100.0;
      }
      set
      {
        if (msFrmSplash == null) return;
        msFrmSplash.mDblLastCompletionFraction = msFrmSplash.mDblCompletionFraction;
        msFrmSplash.mDblCompletionFraction = value;
      }
    }

    
    /// <summary>
    /// Constructor
    /// </summary>
    public SuperStarSplash()
    {
      InitializeComponent();

      Opacity = 0.0;
      timer1.Interval = TimerInterval;
      timer1.Start();
      ClientSize = BackgroundImage.Size;
    }

    public static void ShowSplashScreen()
    {
      if (msFrmSplash != null)
      {
        return;
      }

      msOThread = new Thread(ShowSplash) {IsBackground = true};
      msOThread.SetApartmentState(ApartmentState.STA);
      msOThread.Start();
    }

    public static void CloseSplash()
    {
      if (msFrmSplash != null && msFrmSplash.IsDisposed == false)
      {
        msFrmSplash.mDblOpacityIncrement = -MDblOpacityDecrement;
      }
      
      msOThread = null;
      msFrmSplash = null;
    }

    public static void SetStatus(string newStatus)
    {
      SetStatus(newStatus, true);
    }

    public static void SetStatus(string newStatus, bool setReference)
    {
      if (msFrmSplash == null) return;
      msFrmSplash.mSStatus = newStatus;
      if (setReference) msFrmSplash.SetReferenceInternal();
    }
    
    public static void SetReferencePoint()
    {
      msFrmSplash?.SetReferenceInternal();
    }

    private static void ShowSplash()
    {
      msFrmSplash = new SuperStarSplash();
      Application.Run(msFrmSplash);
    }

    private void SetReferenceInternal()
    {
      if (mBDtSet == false)
      {
        mBDtSet = true;
        mDtStart = DateTime.Now;
        ReadIncrements();
      }

      var dblMilliseconds = ElapsedMilliseconds();
      mAlActualTimes.Add(dblMilliseconds);
      mDblLastCompletionFraction = mDblCompletionFraction;
      if (mAlPreviousCompletionFraction != null && mIIndex < mAlPreviousCompletionFraction.Count)
        mDblCompletionFraction = (double)mAlPreviousCompletionFraction[mIIndex++];
      else
        mDblCompletionFraction = (mIIndex > 0) ? 1 : 0;
    }

    // Milliseconds since the SplashScreen was launched
    private double ElapsedMilliseconds()
    {
      var ts = DateTime.Now - mDtStart;
      return ts.TotalMilliseconds;
    }

    private void ReadIncrements()
    {
      var sPbIncrementPerTimerInterval = SplashRegistryAccess.GetStringRegistryValue(RegvaluePbMillisecondIncrement, "0.0015");
      double dblResult;
      
      mDblPbIncrementPerTimerInterval = 
        double.TryParse(sPbIncrementPerTimerInterval, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out dblResult) 
        ? dblResult : 0.0015;

      var sPbPreviousPctComplete = SplashRegistryAccess.GetStringRegistryValue(RegvaluePbPercents, string.Empty);
      if (sPbPreviousPctComplete.Equals(string.Empty))
      {
        mBFirstLaunch = true;
        return;
      }

      var aTimes = sPbPreviousPctComplete.Split(null);
      mAlPreviousCompletionFraction = new ArrayList();

      foreach (var t in aTimes)
      {
        double dblVal;
        mAlPreviousCompletionFraction.Add(
          double.TryParse(t, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out dblVal)
          ? dblVal : 1.0);
      }
    }

    private void StoreIncrements()
    {
      var sPercent = string.Empty;
      var dblElapsedMilliseconds = ElapsedMilliseconds();

      sPercent = mAlActualTimes.Cast<object>()
        .Aggregate(sPercent, (current, t) => current + ((double) t/dblElapsedMilliseconds)
          .ToString("0.####", NumberFormatInfo.InvariantInfo) + " ");

      SplashRegistryAccess.SetStringRegistryValue(RegvaluePbPercents, sPercent);
      mDblPbIncrementPerTimerInterval = 1.0 / mIActualTicks;
      SplashRegistryAccess.SetStringRegistryValue(RegvaluePbMillisecondIncrement, 
        mDblPbIncrementPerTimerInterval.ToString("#.000000", NumberFormatInfo.InvariantInfo));
    }
    
    private void timer1_Tick(object sender, EventArgs e)
    {
      lblStatus.Text = mSStatus;

      if (mDblOpacityIncrement > 0)
        ProcessOpacityIncrement();
      else
        ProcessDoubleOpacityIncrement();

      if (mBFirstLaunch || !(mDblLastCompletionFraction < mDblCompletionFraction)) return;

      mDblLastCompletionFraction += mDblPbIncrementPerTimerInterval;
      var width = (int)Math.Floor(pnlStatus.ClientRectangle.Width * mDblLastCompletionFraction);
      var height = pnlStatus.ClientRectangle.Height;
      var x = pnlStatus.ClientRectangle.X;
      var y = pnlStatus.ClientRectangle.Y;
      if (width <= 0 || height <= 0) return;

      mRProgress = new Rectangle(x, y, width, height);
      pnlStatus.Invalidate(mRProgress);
      var iSecondsLeft = 1 + (int)(TimerInterval * ((1.0 - mDblLastCompletionFraction) / mDblPbIncrementPerTimerInterval)) / 1000;
      lblTimeRemaining.Text = $"{iSecondsLeft} second{(iSecondsLeft == 1 ? string.Empty : "s")} remaining";
    }

    private void ProcessOpacityIncrement()
    {
      mIActualTicks++;
      if (Opacity < 1) Opacity += mDblOpacityIncrement;
    }

    private void ProcessDoubleOpacityIncrement()
    {
      if (Opacity > 0)
        Opacity += mDblOpacityIncrement;
      else
        CloseOpacityIncrements();
    }

    private void CloseOpacityIncrements()
    {
      StoreIncrements();
      Close();
    }

    private void pnlStatus_Paint(object sender, PaintEventArgs e)
    {
      if (mBFirstLaunch || e.ClipRectangle.Width <= 0 || mIActualTicks <= 1) return;

      var brBackground = 
        new LinearGradientBrush(mRProgress, Color.FromArgb(100, 100, 100), Color.FromArgb(150, 150, 255), LinearGradientMode.Horizontal);
      e.Graphics.FillRectangle(brBackground, mRProgress);
    }

    private void SuperStarSplashDoubleClick(object sender, EventArgs e)
    {
      CloseSplash();
    }
  }
}